package org.jsona4j.annotations;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;

@SuppressWarnings("unchecked")
public class UnMarshaller {
	
	public static <T> T unmarshall(Class<T> c, InputStream in) throws JSONBindException {
		return unmarshall(c, new InputStreamReader(in));
	}

	public static <T> T unmarshall(Class<T> c, Reader reader) throws JSONBindException {
		JSONObject source;
		try {
			JSONTokener tokener = new JSONTokener(reader);
			source = new JSONObject(tokener);
		} catch (Exception e) {
			throw new JSONBindException(e, "Failed to Unmarshall JSON data");
		}
		return (T) convert(c, source);
	}
	
	protected static <T> T convert(Class<T> targetClass, JSONObject jsonObj) throws JSONBindException {
		if (targetClass.isAnnotationPresent(org.jsona4j.annotations.JSONObject.class)) {
			try {
				T target = targetClass.newInstance();
				Class superClass = targetClass;
				while (superClass != null) {
					if (superClass.isAnnotationPresent(org.jsona4j.annotations.JSONObject.class)) {
						for (Field f : superClass.getDeclaredFields()) {
							f.setAccessible(true);
							if (f.isAnnotationPresent(JSONValue.class)) {
								f.set(target, convert(f, jsonObj));
							}
						}
					} else {
						break;
					}
					superClass = superClass.getSuperclass();
				}
				superClass = targetClass;
				while (superClass != null) {
					if (superClass.isAnnotationPresent(org.jsona4j.annotations.JSONObject.class)) {
						for (Method m : superClass.getDeclaredMethods()) {
							m.setAccessible(false);
							if (m.isAnnotationPresent(JSONValue.class) && isSetter(m)) {
								m.invoke(target, convert(m, jsonObj));
							}
						}
					} else {
						break;
					}
					superClass = superClass.getSuperclass();
				}
				return target;
			} catch (Exception e) {
				throw new JSONBindException(e, "Unable to create new instance of %s", targetClass.getSimpleName());
			}
		}
		throw new JSONBindException("Unable to bind JSONObject to %s, missing @JSONObject annotation.", targetClass.getName());
	}
	
	protected static Object convert(Method m, JSONObject jsonObj) throws Exception {
		JSONValue jvalue = m.getAnnotation(JSONValue.class);
		String name = jvalue.value();
		if (name.trim().length() == 0){
			StringBuilder builder = new StringBuilder(m.getName());
			char letter = builder.charAt(3);
			builder.delete(0, 3);
			builder.setCharAt(0, Character.toLowerCase(letter));
			name = builder.toString();
		}
		if (jsonObj.has(name)) {
			Type type = m.getGenericParameterTypes()[0];
			return convert(type, jsonObj.get(name));
		}
		return null;
	}
	
	protected static Object convert(Field f, JSONObject jsonObj) throws Exception {
		JSONValue jvalue = f.getAnnotation(JSONValue.class);
		String name = jvalue.value();
		name = (name.trim().length() == 0) ? f.getName() : name;
		if (jsonObj.has(name)) {
			Type type = f.getGenericType();
			return convert(type, jsonObj.get(name));
		}
		return null;
	}
	
	protected static Object convert(Type type, Object source) throws Exception {
		//debug(type);
		Object value = null;

		if (type instanceof Class) {
			
			Class cType = (Class) type;
			
			// is Array?
			if (cType.isArray()) {
				if (source instanceof JSONArray) {
					JSONArray jsonArray = (JSONArray) source;
					Class componentType = cType.getComponentType();
					value = Array.newInstance(componentType, jsonArray.length());
					for (int ndx=0; ndx<jsonArray.length(); ndx++) {
						Array.set(value, ndx, convert(componentType, jsonArray.get(ndx)));
					}
				} else {
					throw new JSONBindException("Expecting JSONArray source type, instead found %s", source.getClass());
				}
			}
			// is type JSONObject? if so just pass through w/out conversion
			else if (cType.isAssignableFrom(JSONObject.class)) {
				if (source instanceof JSONObject)
					value = source;
				else
					throw new JSONBindException("Expecting JSONObject source type, instead found %s", source.getClass());
			}
			// is type JSONArray? if so just pass through w/out conversion
			else if (cType.isAssignableFrom(JSONArray.class)) {
				if (source instanceof JSONArray)
					value = source;
				else
					throw new JSONBindException("Expecting JSONArray source type, instead found %s", source.getClass());
			}
			// is type annotated with @JSONObject?
			else if (cType.isAnnotationPresent(org.jsona4j.annotations.JSONObject.class)) {
				if (source instanceof JSONObject)
					value = convert(cType, (JSONObject) source);
				else
					throw new JSONBindException("Expecting JSONObject source type, instead found %s", source.getClass());
			}
			
			else {
				Method valueOfMethod = null;
				try {
					valueOfMethod = cType.getDeclaredMethod("valueOf", String.class);
					value = valueOfMethod.invoke(null, source.toString());
				} catch (Exception e) {
					Constructor constructor = null;
					try {
						constructor = cType.getConstructor(String.class);
						value = constructor.newInstance(source.toString());
					} catch (Exception e1) {
						throw new JSONBindException(
								"Unabled to bind %s to %s using %s or %s", type, source.getClass(), valueOfMethod, constructor);
					}
				}
			}
		} else if (type instanceof GenericArrayType) {
			GenericArrayType genArrayType = (GenericArrayType) type;
			Type componentType = genArrayType.getGenericComponentType();
			if (source instanceof JSONArray) {
				JSONArray jsonArray = (JSONArray) source;
				value = Array.newInstance((Class)componentType, jsonArray.length());
				for (int ndx=0; ndx<jsonArray.length(); ndx++) {
					Array.set(value, ndx, convert(componentType, jsonArray.get(ndx)));
				}
			} else {
				throw new JSONBindException("Expecting JSONArray source type, instead found %s", source.getClass());
			}
			
		} else if (type instanceof ParameterizedType) {
			// is Generic List<?> or Map<?,?> ?
			ParameterizedType pType = (ParameterizedType) type;
			if (pType.getRawType() instanceof Class) {
				// is Generic List ?
				Class rawType = (Class) pType.getRawType();
				if (rawType.isAssignableFrom(List.class)) {
					
					if (source instanceof JSONArray) {
						
						JSONArray jsonArray = (JSONArray) source;
						Type argType = pType.getActualTypeArguments()[0];
						List list = createList(rawType);
						for (int ndx=0; ndx<jsonArray.length(); ndx++) {
							list.add(convert(argType, jsonArray.get(ndx)));
						}
						value = list;
						
					} else {
						throw new JSONBindException("Expecting JSONArray source type, instead found %s", source.getClass());
					}
					
				} else if (rawType.isAssignableFrom(Map.class)) {
					
					if (source instanceof JSONObject) {
						
						JSONObject jsonObject = (JSONObject) source;
						Type keyType = pType.getActualTypeArguments()[0];
						Type valType = pType.getActualTypeArguments()[1];
						Map map = createMap(rawType);
						for (int ndx=0; ndx<jsonObject.names().length(); ndx++) {
							String name = jsonObject.names().getString(ndx);
							Object key = convert(keyType, name);
							Object val = convert(valType, jsonObject.get(name));
							map.put(key, val);
						}
						value = map;
						
					} else {
						throw new JSONBindException("Expecting JSONObject source type, instead found %s", source.getClass());
					}
					
				} else {
					throw new JSONBindException("Unrecognized bind type %s", type.getClass());
				}
			}
			
		} 
		return value;
	}
	
	protected static Map createMap(Class mapType) throws Exception {
		Map map = null;
		if (mapType.isInterface()) {
			map = new HashMap();
		} else {
			map = (Map) mapType.newInstance();
		}
		return map;
	}
	
	protected static List createList(Class listType) throws Exception {
		List list = null;
		if (listType.isInterface()) {
			list = new ArrayList();
		} else {
			list = (List) listType.newInstance();
		}
		return list;
	}
	
	protected  static boolean isSetter(Method method) {
		if (!method.getName().startsWith("set"))
			return false;
		if (method.getParameterTypes().length != 1)
			return false;
		if (method.getReturnType() != void.class)
			return false;
		return true;
	}
	
	protected static void debug(Type type) {
		System.out.println("\n"+ type);
		System.out.println("\tClass: " + type.getClass().getName());
		for (Class c : type.getClass().getInterfaces()) {
			System.out.println("\t>> " + c);			
		}
		if (type instanceof ParameterizedType) {
			ParameterizedType pType = (ParameterizedType)type;
			Type t = pType.getRawType();
			System.out.println("\tRawType: " + t + " [" + t.getClass().getName() + "]");
			t = pType.getOwnerType();
			if (t != null)
				System.out.println("\tOwnerType: " + t + " [" + t.getClass().getName() + "]");
			System.out.println("\t-- ActualTypeArguments --");
			for (Type actualtype : pType.getActualTypeArguments()) {
				System.out.println("\t>> " + actualtype + " [" + actualtype.getClass().getName() + "]");	
			}
		} 
	}

}
